from django.db import models
from django.contrib.auth.models import User
from django.contrib import admin

from history.helpers import chain_fields, iter_original_fields
from lib.fields import PickledManyToManyField

def create_model(name, fields=None, app_label='', module='', options=None, admin_opts=None):
    """
    Create specified model
    """
    class Meta:
        # Using type('Meta', ...) gives a dictproxy error during model creation
        pass

    if app_label:
        # app_label must be set using the Meta inner class
        setattr(Meta, 'app_label', app_label)

    # Update Meta with any options that were provided
    if options is not None:
        for key, value in options.iteritems():
            setattr(Meta, key, value)

    # Set up a dictionary to simulate declarations within a class
    attrs = {'__module__': module, 'Meta': Meta}

    # Add in any fields that were provided
    if fields:
        attrs.update(fields)

    # Create the class, which automatically triggers ModelBase processing
    model = type(name, (models.Model,), attrs)

    # Create an Admin class if admin options were provided
    if admin_opts is not None:
        class Admin(admin.ModelAdmin):
            pass
        for key, value in admin_opts:
            setattr(Admin, key, value)
        admin.site.register(model, Admin)

    return model


standard_options = {
    'get_latest_by': 'hst_saved',
    'order_with_respect_to': 'hst_original',
    'ordering': '-hst_revision',
    'unique_together': (('hst_revision', 'hst_original'),),
}


invalid_field_settings = set((
    'creation_counter',
    'primary_key',
    'unique',
    '_unique',
    'unique_for_date',
    'unique_for_month',
    'unique_for_year',
    'rel',
    'attname',
    'db_column',
    'db_index',
    'column',
    'validator_list',
    'related_query_name',
    'm2m_db_table',
    'm2m_column_name',
    'm2m_reverse_name',
    'db_table',
))


def filter_field_settings(settings):
    """
    Filter out invalid field settings (that will prevent archivation)
    """
    filtered = {}
    for k, v in settings.iteritems():
        if k == '_choices':
            filtered['choices'] = v
        elif k == 'related_name':
            filtered[k] = 'history_%s' % v
        elif k not in invalid_field_settings:
            filtered[k] = v
    return filtered


def create_field(base_field):
    kwargs = filter_field_settings(base_field.__dict__)
    # related field wants target model as non-keyword arg, i.e.
    # models.ForeignKey(Location)
    if isinstance(base_field, models.fields.related.RelatedField):
        args = (base_field.rel.to,)
        if base_field.rel.related_name:
            kwargs['related_name'] = 'history_%s' % base_field.rel.related_name
    else:
        args = ()
    return history_class_for_field(base_field)(*args, **kwargs)


def history_class_for_field(base_field):
    if isinstance(base_field, models.ManyToManyField):
        return PickledManyToManyField
    return base_field.__class__


def create_fields(Target, exclude_fields):
    fields = dict((field.name, create_field(field)) for field in chain_fields(Target)
                  if field.name not in exclude_fields)
    stored_fields = set(fields.keys())
    fields['hst_comment'] = models.CharField(blank=True, max_length=128, verbose_name=u'Change comment')
    fields['hst_ip'] = models.IPAddressField(default='0.0.0.0', verbose_name=u'Author IP')
    fields['hst_original'] = models.ForeignKey(Target, verbose_name=u'Original')
    fields['hst_revision'] = models.IntegerField(default=-1, editable=False, verbose_name=u'Revision')
    fields['hst_saved'] = models.DateTimeField(auto_now=True, verbose_name=u'Change time')
    fields['hst_user'] = models.ForeignKey(User, null=True, blank=True, verbose_name=u'Author')
    fields['__unicode__'] = lambda self: u'%s [%d]' % (self.hst_original, self.hst_revision)
    def clone_next_revision(self, **kwargs):
        next = self.__class__(hst_original=self.hst_original, hst_revision=self.hst_revision+1, **kwargs)
        for field in iter_original_fields(self):
            setattr(next, field.attname, getattr(self, field.attname))
        return next
    fields['clone_next_revision'] = clone_next_revision
    fields['is_first'] = lambda self: self.hst_revision == 1
    fields['hst_revision_prev'] = lambda self: self.hst_revision - 1
    return fields, stored_fields


def create_history_for_model(Target, exclude_fields):
    """
    Mark model for archivation
    """
    fields, stored_fields = create_fields(Target, exclude_fields)
    return create_model(
        'History%s' % Target.__name__,
        fields=fields,
        app_label=Target._meta.app_label,
        module=Target.__module__,
        options=standard_options,
        admin_opts={},
    ), stored_fields
